Coverage Report

Created: 2026-06-19 16:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\src\utils\windows.rs
Line
Count
Source
1
//! Windows API abstraction layer for console and system operations.
2
//!
3
//! This module provides a trait-based abstraction over Windows APIs to enable
4
//! mocking in tests and centralize Windows-specific functionality.
5
6
#![deny(clippy::implicit_return)]
7
#![allow(
8
    clippy::needless_return,
9
    clippy::doc_overindented_list_items,
10
    rustdoc::private_intra_doc_links
11
)]
12
13
use log::{error, warn};
14
use std::ffi::OsString;
15
use std::os::windows::ffi::OsStrExt;
16
use std::{mem, ptr};
17
18
use windows::core::{BOOL, HSTRING, PCWSTR};
19
use windows::Win32::Foundation::{COLORREF, FALSE, HANDLE, HWND, LPARAM, TRUE};
20
use windows::Win32::Graphics::Dwm::{DwmSetWindowAttribute, DWMWA_BORDER_COLOR};
21
use windows::Win32::Graphics::Gdi::InvalidateRect;
22
use windows::Win32::System::Console::{
23
    FillConsoleOutputAttribute, GetConsoleProcessList, GetConsoleScreenBufferInfo,
24
    GetConsoleWindow, GetStdHandle, ReadConsoleInputW, SetConsoleTextAttribute,
25
    CONSOLE_CHARACTER_ATTRIBUTES, CONSOLE_SCREEN_BUFFER_INFO, COORD, INPUT_RECORD, INPUT_RECORD_0,
26
    STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
27
};
28
use windows::Win32::System::Console::{GetConsoleMode, SetConsoleMode, CONSOLE_MODE};
29
use windows::Win32::System::Console::{
30
    ScrollConsoleScreenBufferW, SetConsoleCursorPosition, CHAR_INFO, KEY_EVENT as KEY_EVENT_U32,
31
    SMALL_RECT,
32
};
33
use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
34
use windows::Win32::System::Threading::{
35
    CreateProcessW, CREATE_NEW_CONSOLE, PROCESS_INFORMATION, STARTF_USESHOWWINDOW, STARTUPINFOW,
36
};
37
use windows::Win32::System::Threading::{GetExitCodeProcess, OpenProcess};
38
use windows::Win32::UI::WindowsAndMessaging::{
39
    BringWindowToTop, EnumWindows, GetForegroundWindow, GetWindowPlacement, GetWindowTextW,
40
    GetWindowThreadProcessId, MoveWindow, SetWindowPos, SetWindowTextW, ShowWindow, HWND_NOTOPMOST,
41
    HWND_TOPMOST, SHOW_WINDOW_CMD, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SW_SHOWNOACTIVATE,
42
    SYSTEM_METRICS_INDEX, WINDOWPLACEMENT,
43
};
44
45
#[cfg(test)]
46
use mockall::automock;
47
48
use super::constants::MAX_WINDOW_TITLE_LENGTH;
49
50
/// Trait for Windows API operations to enable mocking in tests.
51
///
52
/// This trait abstracts Windows API calls to allow for unit testing without
53
/// actual system interaction. All console and system operations should go
54
/// through this trait.
55
#[cfg_attr(test, automock)]
56
pub trait WindowsApi: Send + Sync {
57
    /// Sets the console window title.
58
    ///
59
    /// # Arguments
60
    ///
61
    /// * `title` - The string to be set as window title
62
    ///
63
    /// # Returns
64
    ///
65
    /// Result indicating success or failure of the operation
66
    fn set_console_title(&self, title: &str) -> windows::core::Result<()>;
67
68
    /// Gets the console window title as UTF-16 buffer.
69
    ///
70
    /// # Arguments
71
    ///
72
    /// * `buffer` - Mutable buffer to store the UTF-16 encoded title
73
    ///
74
    /// # Returns
75
    ///
76
    /// Number of characters copied to the buffer
77
    fn get_console_title(&self, buffer: &mut [u16]) -> i32;
78
79
    /// Gets OS version string.
80
    ///
81
    /// # Returns
82
    ///
83
    /// String representation of the OS version
84
    fn get_os_version(&self) -> String;
85
86
    /// Arranges the console window position and size.
87
    ///
88
    /// # Arguments
89
    ///
90
    /// * `x` - The x coordinate to move the window to
91
    /// * `y` - The y coordinate to move the window to
92
    /// * `width` - The width in pixels to resize the window to
93
    /// * `height` - The height in pixels to resize the window to
94
    ///
95
    /// # Returns
96
    ///
97
    /// Result indicating success or failure of the operation
98
    fn arrange_console(&self, x: i32, y: i32, width: i32, height: i32)
99
        -> windows::core::Result<()>;
100
101
    /// Sets console text attribute.
102
    ///
103
    /// # Arguments
104
    ///
105
    /// * `attributes` - Console character attributes to set
106
    ///
107
    /// # Returns
108
    ///
109
    /// Result indicating success or failure of the operation
110
    fn set_console_text_attribute(
111
        &self,
112
        attributes: CONSOLE_CHARACTER_ATTRIBUTES,
113
    ) -> windows::core::Result<()>;
114
115
    /// Gets console screen buffer info.
116
    ///
117
    /// # Returns
118
    ///
119
    /// Console screen buffer information or error
120
    fn get_console_screen_buffer_info(&self) -> windows::core::Result<CONSOLE_SCREEN_BUFFER_INFO>;
121
122
    /// Fills console output with specified attribute.
123
    ///
124
    /// # Arguments
125
    ///
126
    /// * `attribute` - Attribute to fill with
127
    /// * `length` - Number of characters to fill
128
    /// * `coord` - Starting coordinate
129
    ///
130
    /// # Returns
131
    ///
132
    /// Number of characters actually filled or error
133
    fn fill_console_output_attribute(
134
        &self,
135
        attribute: u16,
136
        length: u32,
137
        coord: COORD,
138
    ) -> windows::core::Result<u32>;
139
140
    /// Scrolls console screen buffer.
141
    ///
142
    /// # Arguments
143
    ///
144
    /// * `scroll_rect` - Rectangle to scroll
145
    /// * `scroll_target` - Target coordinate for scrolling
146
    /// * `fill_char` - Character to fill empty space with
147
    ///
148
    /// # Returns
149
    ///
150
    /// Result indicating success or failure of the operation
151
    fn scroll_console_screen_buffer(
152
        &self,
153
        scroll_rect: SMALL_RECT,
154
        scroll_target: COORD,
155
        fill_char: CHAR_INFO,
156
    ) -> windows::core::Result<()>;
157
158
    /// Sets console cursor position.
159
    ///
160
    /// # Arguments
161
    ///
162
    /// * `position` - New cursor position
163
    ///
164
    /// # Returns
165
    ///
166
    /// Result indicating success or failure of the operation
167
    fn set_console_cursor_position(&self, position: COORD) -> windows::core::Result<()>;
168
169
    /// Gets standard handle.
170
    ///
171
    /// # Arguments
172
    ///
173
    /// * `handle_type` - Type of standard handle to retrieve
174
    ///
175
    /// # Returns
176
    ///
177
    /// Handle to the requested standard device or error
178
    fn get_std_handle(&self, handle_type: STD_HANDLE) -> windows::core::Result<HANDLE>;
179
180
    /// Reads console input.
181
    ///
182
    /// # Arguments
183
    ///
184
    /// * `buffer` - Buffer to store input records
185
    ///
186
    /// # Returns
187
    ///
188
    /// Number of records read or error
189
    fn read_console_input(&self, buffer: &mut [INPUT_RECORD]) -> windows::core::Result<u32>;
190
191
    /// Sets DWM window attribute for border color.
192
    ///
193
    /// # Arguments
194
    ///
195
    /// * `color` - Color to set as border color
196
    ///
197
    /// # Returns
198
    ///
199
    /// Result indicating success or failure of the operation
200
    fn set_console_border_color(&self, color: &COLORREF) -> windows::core::Result<()>;
201
202
    /// Marks the entire console window client area as needing a redraw.
203
    ///
204
    /// Used to nudge the legacy Win10 conhost into repainting from its
205
    /// own buffer state after bulk attribute changes that the renderer
206
    /// otherwise leaves stale on the trailing row/column.
207
    ///
208
    /// # Returns
209
    ///
210
    /// Result indicating success or failure of the operation
211
    fn invalidate_console_window(&self) -> windows::core::Result<()>;
212
213
    /// Writes input records to the console input buffer.
214
    ///
215
    /// # Arguments
216
    ///
217
    /// * `buffer` - Input records to write
218
    /// * `number_written` - Mutable reference to store number of records written
219
    ///
220
    /// # Returns
221
    ///
222
    /// Result indicating success or failure of the operation
223
    fn write_console_input(
224
        &self,
225
        buffer: &[INPUT_RECORD],
226
        number_written: &mut u32,
227
    ) -> windows::core::Result<()>;
228
229
    /// Gets the last Windows error code.
230
    ///
231
    /// # Returns
232
    ///
233
    /// The last error code from Windows API
234
    fn get_last_error(&self) -> u32;
235
236
    /// Generates a console control event.
237
    ///
238
    /// # Arguments
239
    ///
240
    /// * `ctrl_event` - Control event type to generate
241
    /// * `process_group_id` - Process group ID to send event to
242
    ///
243
    /// # Returns
244
    ///
245
    /// Result indicating success or failure of the operation
246
    fn generate_console_ctrl_event(
247
        &self,
248
        ctrl_event: u32,
249
        process_group_id: u32,
250
    ) -> windows::core::Result<()>;
251
252
    /// Get standard output handle
253
    ///
254
    /// # Returns
255
    ///
256
    /// Handle to standard output or error
257
    fn get_stdout_handle(&self) -> windows::core::Result<HANDLE>;
258
259
    /// Get console screen buffer information
260
    ///
261
    /// # Arguments
262
    ///
263
    /// * `handle` - Handle to console screen buffer
264
    ///
265
    /// # Returns
266
    ///
267
    /// Console screen buffer information or error
268
    fn get_console_attached_process_count(&self) -> u32;
269
270
    /// Create a new process attached to its own console window.
271
    ///
272
    /// When `with_keyboard_focus` is false the new console window is
273
    /// shown without activation (`STARTF_USESHOWWINDOW` +
274
    /// `SW_SHOWNOACTIVATE`), so the caller retains keyboard focus.
275
    /// Used when the daemon spawns client consoles - otherwise the
276
    /// last-spawned client wins the foreground and Windows refuses to
277
    /// let the daemon steal it back.
278
    ///
279
    /// # Arguments
280
    ///
281
    /// * `application`         - Application name including file extension
282
    /// * `args`                - List of arguments to the application
283
    /// * `with_keyboard_focus` - Whether the new console window should take
284
    ///                           foreground focus when it appears.
285
    ///
286
    /// # Returns
287
    ///
288
    /// Process information if successful, None otherwise
289
1
    fn create_process_with_args(
290
1
        &self,
291
1
        application: &str,
292
1
        args: Vec<String>,
293
1
        with_keyboard_focus: bool,
294
1
    ) -> Option<windows::Win32::System::Threading::PROCESS_INFORMATION> {
295
1
        let command_line = build_command_line(application, &args);
296
1
        let mut startupinfo = build_startupinfo(with_keyboard_focus);
297
1
        let mut process_information = PROCESS_INFORMATION::default();
298
1
        let mut cmd_line = command_line;
299
1
        let command_line_ptr = windows::core::PWSTR(cmd_line.as_mut_ptr());
300
301
1
        match self.create_process_raw(
302
1
            application,
303
1
            command_line_ptr,
304
1
            &mut startupinfo,
305
1
            &mut process_information,
306
1
        ) {
307
1
            Ok(()) => return Some(process_information),
308
0
            Err(_) => return None,
309
        }
310
1
    }
311
312
    /// Low-level process creation API call
313
    ///
314
    /// # Arguments
315
    ///
316
    /// * `application` - Application name
317
    /// * `command_line` - Command line string as PWSTR
318
    /// * `startup_info` - Startup information structure
319
    /// * `process_info` - Process information structure to fill
320
    ///
321
    /// # Returns
322
    ///
323
    /// Result indicating success or failure of the operation
324
    fn create_process_raw(
325
        &self,
326
        application: &str,
327
        command_line: windows::core::PWSTR,
328
        startup_info: &mut windows::Win32::System::Threading::STARTUPINFOW,
329
        process_info: &mut windows::Win32::System::Threading::PROCESS_INFORMATION,
330
    ) -> windows::core::Result<()>;
331
332
    /// Get window handle for process ID
333
    ///
334
    /// # Arguments
335
    ///
336
    /// * `process_id` - Process ID to find window for
337
    ///
338
    /// # Returns
339
    ///
340
    /// Window handle for the process
341
    fn get_window_handle_for_process(&self, process_id: u32) -> HWND;
342
343
    /// Gets the console window handle.
344
    ///
345
    /// # Returns
346
    ///
347
    /// Handle to the console window
348
    fn get_console_window(&self) -> HWND;
349
350
    /// Gets the foreground window handle.
351
    ///
352
    /// # Returns
353
    ///
354
    /// Handle to the foreground window
355
    fn get_foreground_window(&self) -> HWND;
356
357
    /// Bring `hwnd` to the top of the z-order.
358
    ///
359
    /// When `with_keyboard_focus` is true the window is also activated and
360
    /// receives keyboard focus. When false the window is raised without
361
    /// activation, avoiding the taskbar flash that would happen if the
362
    /// window is not the current input target.
363
    ///
364
    /// # Arguments
365
    ///
366
    /// * `hwnd`                - Handle to the window to raise.
367
    /// * `with_keyboard_focus` - Whether to activate the window and give
368
    ///                           it keyboard focus.
369
    ///
370
    /// # Returns
371
    ///
372
    /// Result indicating success or failure of the operation
373
    fn bring_window_to_top(
374
        &self,
375
        hwnd: HWND,
376
        with_keyboard_focus: bool,
377
    ) -> windows::core::Result<()>;
378
379
    /// Gets console mode for the specified handle.
380
    ///
381
    /// # Arguments
382
    ///
383
    /// * `handle` - Handle to the console input buffer
384
    ///
385
    /// # Returns
386
    ///
387
    /// Console mode or error
388
    fn get_console_mode(&self, handle: HANDLE) -> windows::core::Result<CONSOLE_MODE>;
389
390
    /// Sets console mode for the specified handle.
391
    ///
392
    /// # Arguments
393
    ///
394
    /// * `handle` - Handle to the console input buffer
395
    /// * `mode` - Console mode to set
396
    ///
397
    /// # Returns
398
    ///
399
    /// Result indicating success or failure of the operation
400
    fn set_console_mode(&self, handle: HANDLE, mode: CONSOLE_MODE) -> windows::core::Result<()>;
401
402
    /// Gets the exit code of the specified process.
403
    ///
404
    /// # Arguments
405
    ///
406
    /// * `handle` - Handle to the process
407
    ///
408
    /// # Returns
409
    ///
410
    /// Exit code or error
411
    fn get_exit_code(&self, handle: HANDLE) -> windows::core::Result<u32>;
412
413
    /// Moves and resizes a window.
414
    ///
415
    /// # Arguments
416
    ///
417
    /// * `hwnd` - Handle to the window
418
    /// * `x` - New x position
419
    /// * `y` - New y position
420
    /// * `width` - New width
421
    /// * `height` - New height
422
    /// * `repaint` - Whether to repaint the window
423
    ///
424
    /// # Returns
425
    ///
426
    /// Result indicating success or failure of the operation
427
    fn move_window(
428
        &self,
429
        hwnd: HWND,
430
        x: i32,
431
        y: i32,
432
        width: i32,
433
        height: i32,
434
        repaint: bool,
435
    ) -> windows::core::Result<()>;
436
437
    /// Gets window placement information.
438
    ///
439
    /// # Arguments
440
    ///
441
    /// * `hwnd` - Handle to the window
442
    ///
443
    /// # Returns
444
    ///
445
    /// Window placement information or error
446
    fn get_window_placement(&self, hwnd: HWND) -> windows::core::Result<WINDOWPLACEMENT>;
447
448
    /// Shows a window in the specified state.
449
    ///
450
    /// # Arguments
451
    ///
452
    /// * `hwnd` - Handle to the window
453
    /// * `cmd_show` - Show command
454
    ///
455
    /// # Returns
456
    ///
457
    /// Result indicating success or failure of the operation
458
    fn show_window(&self, hwnd: HWND, cmd_show: SHOW_WINDOW_CMD) -> windows::core::Result<bool>;
459
460
    /// Checks if a window handle is valid.
461
    ///
462
    /// # Arguments
463
    ///
464
    /// * `hwnd` - Handle to the window to check
465
    ///
466
    /// # Returns
467
    ///
468
    /// True if the window is valid, false otherwise
469
    fn is_window(&self, hwnd: HWND) -> bool;
470
471
    /// Opens a process with the specified access rights.
472
    ///
473
    /// # Arguments
474
    ///
475
    /// * `access` - Access rights for the process handle
476
    /// * `inherit` - Whether the handle can be inherited
477
    /// * `process_id` - Process ID to open
478
    ///
479
    /// # Returns
480
    ///
481
    /// Process handle or error
482
    fn open_process(
483
        &self,
484
        access: u32,
485
        inherit: bool,
486
        process_id: u32,
487
    ) -> windows::core::Result<HANDLE>;
488
489
    /// Gets system metrics information.
490
    ///
491
    /// # Arguments
492
    ///
493
    /// * `index` - System metric index to retrieve
494
    ///
495
    /// # Returns
496
    ///
497
    /// The requested system metric value
498
    fn get_system_metrics(&self, index: SYSTEM_METRICS_INDEX) -> i32;
499
500
    /// Sets the process DPI awareness.
501
    ///
502
    /// # Arguments
503
    ///
504
    /// * `value` - DPI awareness value to set
505
    ///
506
    /// # Returns
507
    ///
508
    /// Result indicating success or failure of the operation
509
    fn set_process_dpi_awareness(
510
        &self,
511
        value: windows::Win32::UI::HiDpi::PROCESS_DPI_AWARENESS,
512
    ) -> windows::core::Result<()>;
513
}
514
515
#[cfg(test)]
516
impl Clone for MockWindowsApi {
517
0
    fn clone(&self) -> Self {
518
0
        return MockWindowsApi::new();
519
0
    }
520
}
521
522
/// Default implementation of WindowsApi that calls actual Windows APIs.
523
///
524
/// This implementation provides direct access to Windows system APIs and should
525
/// be used in production code. For testing, use the MockWindowsApi instead.
526
#[derive(Clone)]
527
pub struct DefaultWindowsApi;
528
529
#[cfg_attr(coverage_nightly, coverage(off))]
530
impl WindowsApi for DefaultWindowsApi {
531
    fn set_console_title(&self, title: &str) -> windows::core::Result<()> {
532
        return unsafe { SetWindowTextW(GetConsoleWindow(), &HSTRING::from(title)) };
533
    }
534
535
    fn get_console_title(&self, buffer: &mut [u16]) -> i32 {
536
        return unsafe { GetWindowTextW(GetConsoleWindow(), buffer) };
537
    }
538
539
    fn get_os_version(&self) -> String {
540
        return os_info::get().version().to_string();
541
    }
542
543
    fn arrange_console(
544
        &self,
545
        x: i32,
546
        y: i32,
547
        width: i32,
548
        height: i32,
549
    ) -> windows::core::Result<()> {
550
        return unsafe { MoveWindow(GetConsoleWindow(), x, y, width, height, true) };
551
    }
552
553
    fn set_console_text_attribute(
554
        &self,
555
        attributes: CONSOLE_CHARACTER_ATTRIBUTES,
556
    ) -> windows::core::Result<()> {
557
        return unsafe { SetConsoleTextAttribute(self.get_stdout_handle()?, attributes) };
558
    }
559
560
    fn get_console_screen_buffer_info(&self) -> windows::core::Result<CONSOLE_SCREEN_BUFFER_INFO> {
561
        let mut buffer_info = CONSOLE_SCREEN_BUFFER_INFO::default();
562
        unsafe { GetConsoleScreenBufferInfo(self.get_stdout_handle()?, &mut buffer_info)? };
563
        return Ok(buffer_info);
564
    }
565
566
    fn fill_console_output_attribute(
567
        &self,
568
        attribute: u16,
569
        length: u32,
570
        coord: COORD,
571
    ) -> windows::core::Result<u32> {
572
        let mut number_written = 0u32;
573
        unsafe {
574
            FillConsoleOutputAttribute(
575
                self.get_stdout_handle()?,
576
                attribute,
577
                length,
578
                coord,
579
                &mut number_written,
580
            )?
581
        };
582
        return Ok(number_written);
583
    }
584
585
    fn scroll_console_screen_buffer(
586
        &self,
587
        scroll_rect: SMALL_RECT,
588
        scroll_target: COORD,
589
        fill_char: CHAR_INFO,
590
    ) -> windows::core::Result<()> {
591
        return unsafe {
592
            ScrollConsoleScreenBufferW(
593
                self.get_stdout_handle()?,
594
                &scroll_rect,
595
                None,
596
                scroll_target,
597
                &fill_char,
598
            )
599
        };
600
    }
601
602
    fn set_console_cursor_position(&self, position: COORD) -> windows::core::Result<()> {
603
        return unsafe { SetConsoleCursorPosition(self.get_stdout_handle()?, position) };
604
    }
605
606
    fn get_std_handle(&self, handle_type: STD_HANDLE) -> windows::core::Result<HANDLE> {
607
        return unsafe { GetStdHandle(handle_type) };
608
    }
609
610
    fn read_console_input(&self, buffer: &mut [INPUT_RECORD]) -> windows::core::Result<u32> {
611
        let mut number_read = 0u32;
612
        unsafe {
613
            ReadConsoleInputW(
614
                self.get_std_handle(STD_INPUT_HANDLE)?,
615
                buffer,
616
                &mut number_read,
617
            )?
618
        };
619
        return Ok(number_read);
620
    }
621
622
    fn set_console_border_color(&self, color: &COLORREF) -> windows::core::Result<()> {
623
        return unsafe {
624
            DwmSetWindowAttribute(
625
                GetConsoleWindow(),
626
                DWMWA_BORDER_COLOR,
627
                color as *const COLORREF as *const _,
628
                mem::size_of::<COLORREF>() as u32,
629
            )
630
        };
631
    }
632
633
    fn invalidate_console_window(&self) -> windows::core::Result<()> {
634
        let succeeded = unsafe { InvalidateRect(Some(GetConsoleWindow()), None, false) };
635
        if succeeded.as_bool() {
636
            return Ok(());
637
        }
638
        return Err(windows::core::Error::from_thread());
639
    }
640
641
    fn write_console_input(
642
        &self,
643
        buffer: &[INPUT_RECORD],
644
        number_written: &mut u32,
645
    ) -> windows::core::Result<()> {
646
        unsafe {
647
            windows::Win32::System::Console::WriteConsoleInputW(
648
                GetStdHandle(STD_INPUT_HANDLE)?,
649
                buffer,
650
                number_written,
651
            )?
652
        };
653
        return Ok(());
654
    }
655
656
    fn get_last_error(&self) -> u32 {
657
        return unsafe { windows::Win32::Foundation::GetLastError().0 };
658
    }
659
660
    fn generate_console_ctrl_event(
661
        &self,
662
        ctrl_event: u32,
663
        process_group_id: u32,
664
    ) -> windows::core::Result<()> {
665
        return unsafe {
666
            windows::Win32::System::Console::GenerateConsoleCtrlEvent(ctrl_event, process_group_id)
667
        };
668
    }
669
670
    fn get_stdout_handle(&self) -> windows::core::Result<HANDLE> {
671
        return self.get_std_handle(STD_OUTPUT_HANDLE);
672
    }
673
674
    fn get_console_attached_process_count(&self) -> u32 {
675
        let mut value: [u32; 1] = [0];
676
        unsafe { return GetConsoleProcessList(&mut value) };
677
    }
678
679
    fn get_window_handle_for_process(&self, process_id: u32) -> HWND {
680
        /// Data structure for window search callback
681
        struct WindowSearchData {
682
            /// The process ID we're searching for
683
            target_process_id: u32,
684
            /// Mutable reference to store the found window handle
685
            found_handle: *mut Option<HWND>,
686
        }
687
688
        /// Callback function for finding windows by process ID with proper handle capture
689
        unsafe extern "system" fn find_window_callback_with_capture(
690
            hwnd: HWND,
691
            lparam: LPARAM,
692
        ) -> BOOL {
693
            let search_data = &mut *(lparam.0 as *mut WindowSearchData);
694
            let mut window_process_id: u32 = 0;
695
            GetWindowThreadProcessId(hwnd, Some(&mut window_process_id));
696
697
            if search_data.target_process_id == window_process_id {
698
                // Store the found window handle
699
                *search_data.found_handle = Some(hwnd);
700
                return FALSE; // Stop enumeration
701
            }
702
            return TRUE; // Continue enumeration
703
        }
704
705
        let mut found_handle = None;
706
        let mut search_data = WindowSearchData {
707
            target_process_id: process_id,
708
            found_handle: &mut found_handle,
709
        };
710
711
        loop {
712
            let _ = unsafe {
713
                EnumWindows(
714
                    Some(find_window_callback_with_capture),
715
                    LPARAM(&mut search_data as *mut WindowSearchData as isize),
716
                )
717
            };
718
            if let Some(handle) = found_handle {
719
                return handle;
720
            }
721
        }
722
    }
723
724
    fn create_process_raw(
725
        &self,
726
        application: &str,
727
        command_line: windows::core::PWSTR,
728
        startup_info: &mut windows::Win32::System::Threading::STARTUPINFOW,
729
        process_info: &mut windows::Win32::System::Threading::PROCESS_INFORMATION,
730
    ) -> windows::core::Result<()> {
731
        return unsafe {
732
            CreateProcessW(
733
                &HSTRING::from(application),
734
                Some(command_line),
735
                Some(ptr::null_mut()),
736
                Some(ptr::null_mut()),
737
                false,
738
                CREATE_NEW_CONSOLE,
739
                Some(ptr::null_mut()),
740
                PCWSTR::null(),
741
                ptr::addr_of_mut!(*startup_info),
742
                ptr::addr_of_mut!(*process_info),
743
            )
744
        };
745
    }
746
747
    fn get_console_window(&self) -> HWND {
748
        return unsafe { GetConsoleWindow() };
749
    }
750
751
    fn get_foreground_window(&self) -> HWND {
752
        return unsafe { GetForegroundWindow() };
753
    }
754
755
    fn bring_window_to_top(
756
        &self,
757
        hwnd: HWND,
758
        with_keyboard_focus: bool,
759
    ) -> windows::core::Result<()> {
760
        if with_keyboard_focus {
761
            return unsafe { BringWindowToTop(hwnd) };
762
        }
763
        // Raise without activation via the HWND_TOPMOST -> HWND_NOTOPMOST
764
        // trick. Synchronous SetWindowPos (no SWP_ASYNCWINDOWPOS) so
765
        // HWND_TOPMOST is applied before we strip it again - otherwise
766
        // rapid invocations from the z-order loop can leave the
767
        // WS_EX_TOPMOST flag set, floating client windows above other
768
        // applications. See https://stackoverflow.com/questions/5257977.
769
        unsafe {
770
            SetWindowPos(
771
                hwnd,
772
                Some(HWND_TOPMOST),
773
                0,
774
                0,
775
                0,
776
                0,
777
                SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
778
            )?;
779
            SetWindowPos(
780
                hwnd,
781
                Some(HWND_NOTOPMOST),
782
                0,
783
                0,
784
                0,
785
                0,
786
                SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
787
            )?;
788
        }
789
        return Ok(());
790
    }
791
792
    fn get_console_mode(&self, handle: HANDLE) -> windows::core::Result<CONSOLE_MODE> {
793
        let mut mode = CONSOLE_MODE(0u32);
794
        unsafe { GetConsoleMode(handle, &mut mode)? };
795
        return Ok(mode);
796
    }
797
798
    fn set_console_mode(&self, handle: HANDLE, mode: CONSOLE_MODE) -> windows::core::Result<()> {
799
        return unsafe { SetConsoleMode(handle, mode) };
800
    }
801
802
    fn get_exit_code(&self, handle: HANDLE) -> windows::core::Result<u32> {
803
        let mut exit_code: u32 = 0;
804
        unsafe { GetExitCodeProcess(handle, &mut exit_code)? };
805
        return Ok(exit_code);
806
    }
807
808
    fn move_window(
809
        &self,
810
        hwnd: HWND,
811
        x: i32,
812
        y: i32,
813
        width: i32,
814
        height: i32,
815
        repaint: bool,
816
    ) -> windows::core::Result<()> {
817
        return unsafe { MoveWindow(hwnd, x, y, width, height, repaint) };
818
    }
819
820
    fn get_window_placement(&self, hwnd: HWND) -> windows::core::Result<WINDOWPLACEMENT> {
821
        let mut placement: WINDOWPLACEMENT = WINDOWPLACEMENT {
822
            length: mem::size_of::<WINDOWPLACEMENT>() as u32,
823
            ..Default::default()
824
        };
825
        unsafe { GetWindowPlacement(hwnd, &mut placement)? };
826
        return Ok(placement);
827
    }
828
829
    fn show_window(&self, hwnd: HWND, cmd_show: SHOW_WINDOW_CMD) -> windows::core::Result<bool> {
830
        let result = unsafe { ShowWindow(hwnd, cmd_show) };
831
        return Ok(result.as_bool());
832
    }
833
834
    fn is_window(&self, hwnd: HWND) -> bool {
835
        return unsafe { windows::Win32::UI::WindowsAndMessaging::IsWindow(Some(hwnd)).as_bool() };
836
    }
837
838
    fn open_process(
839
        &self,
840
        access: u32,
841
        inherit: bool,
842
        process_id: u32,
843
    ) -> windows::core::Result<HANDLE> {
844
        return unsafe { OpenProcess(PROCESS_ACCESS_RIGHTS(access), inherit, process_id) };
845
    }
846
847
    fn get_system_metrics(&self, index: SYSTEM_METRICS_INDEX) -> i32 {
848
        return unsafe { windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics(index) };
849
    }
850
851
    fn set_process_dpi_awareness(
852
        &self,
853
        value: windows::Win32::UI::HiDpi::PROCESS_DPI_AWARENESS,
854
    ) -> windows::core::Result<()> {
855
        return unsafe { windows::Win32::UI::HiDpi::SetProcessDpiAwareness(value) };
856
    }
857
}
858
859
/// u16 representation of a [KEY_EVENT][1].
860
///
861
/// For some reason the public [KEY_EVENT][1] constant is a u32
862
/// while the [INPUT_RECORD][2].`EventType` is u16...
863
///
864
/// [1]: https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Console/constant.KEY_EVENT.html
865
/// [2]: https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Console/struct.INPUT_RECORD.html
866
pub const KEY_EVENT: u16 = KEY_EVENT_U32 as u16;
867
868
/// Build a `STARTUPINFOW` for [`WindowsApi::create_process_with_args`].
869
///
870
/// When `with_keyboard_focus` is false, `STARTF_USESHOWWINDOW` and
871
/// `SW_SHOWNOACTIVATE` are populated so the new console appears without
872
/// stealing foreground focus. Otherwise the struct is left at its default
873
/// (the new process picks its own show-window behaviour).
874
///
875
/// # Arguments
876
///
877
/// * `with_keyboard_focus` - Whether the spawned process is allowed to take
878
///                           foreground focus when its console appears.
879
///
880
/// # Returns
881
///
882
/// A `STARTUPINFOW` with `cb` set and, when applicable, the no-activate
883
/// show-window flags applied.
884
3
pub(crate) fn build_startupinfo(with_keyboard_focus: bool) -> STARTUPINFOW {
885
3
    let mut startupinfo = STARTUPINFOW {
886
3
        cb: mem::size_of::<STARTUPINFOW>() as u32,
887
3
        ..Default::default()
888
3
    };
889
3
    if !with_keyboard_focus {
890
1
        startupinfo.dwFlags = STARTF_USESHOWWINDOW;
891
1
        startupinfo.wShowWindow = SW_SHOWNOACTIVATE.0 as u16;
892
2
    }
893
3
    return startupinfo;
894
3
}
895
896
/// Build command line string for Windows process creation
897
///
898
/// # Arguments
899
///
900
/// * `application` - Application name including file extension
901
/// * `args` - List of arguments to the application
902
///
903
/// # Returns
904
///
905
/// UTF-16 encoded command line with proper quoting
906
///
907
/// # Examples
908
///
909
/// ```
910
/// use csshw_lib::utils::windows::build_command_line;
911
///
912
/// let cmd_line = build_command_line("cmd.exe", &["arg1".to_string(), "arg2".to_string()]);
913
/// // Returns UTF-16 encoded: "cmd.exe" "arg1" "arg2"\0
914
/// ```
915
4
pub fn build_command_line(application: &str, args: &[String]) -> Vec<u16> {
916
4
    let mut cmd: Vec<u16> = Vec::new();
917
4
    cmd.push(b'"' as u16);
918
4
    cmd.extend(OsString::from(application).encode_wide());
919
4
    cmd.push(b'"' as u16);
920
921
5
    for arg in 
args4
{
922
5
        cmd.push(' ' as u16);
923
5
        cmd.push(b'"' as u16);
924
5
        cmd.extend(OsString::from(arg).encode_wide());
925
5
        cmd.push(b'"' as u16);
926
5
    }
927
4
    cmd.push(0); // add null terminator
928
929
4
    return cmd;
930
4
}
931
932
/// Sets the back- and foreground color of the current console window using the provided API.
933
///
934
/// # Arguments
935
///
936
/// * `api` - The Windows API implementation to use.
937
/// * `color` - The color value describing the back- and foreground color.
938
///
939
/// # Examples
940
///
941
/// ```no_run
942
/// use csshw_lib::utils::windows::{set_console_color, DefaultWindowsApi};
943
/// use windows::Win32::System::Console::CONSOLE_CHARACTER_ATTRIBUTES;
944
///
945
/// let api = DefaultWindowsApi;
946
/// set_console_color(&api, CONSOLE_CHARACTER_ATTRIBUTES(0x0F));
947
/// ```
948
8
pub fn set_console_color(api: &dyn WindowsApi, color: CONSOLE_CHARACTER_ATTRIBUTES) {
949
8
    api.set_console_text_attribute(color).unwrap();
950
8
    let buffer_info = api.get_console_screen_buffer_info().unwrap();
951
    // FillConsoleOutputAttribute continues into successive rows when the
952
    // length extends past the end of the row, so a single call from (0,0)
953
    // recolors the entire buffer in one LPC roundtrip.
954
8
    let width: u32 = buffer_info.dwSize.X.try_into().unwrap();
955
8
    let height: u32 = buffer_info.dwSize.Y.try_into().unwrap();
956
8
    api.fill_console_output_attribute(color.0, width * height, COORD { X: 0, Y: 0 })
957
8
        .unwrap();
958
    // The console client area can contain a sub-cell-sized pixel sliver at
959
    // the right and/or bottom edge when the OS window pixel dimensions do
960
    // not divide evenly into the cell grid (csshw clients are sized in
961
    // pixels via MoveWindow). Those slivers are not backed by any buffer
962
    // cell, so the fill above cannot reach them, and conhost does not
963
    // repaint them on an attribute-only update - they keep the old color
964
    // until something triggers a WM_PAINT (user double-clicking is one
965
    // such trigger).
966
8
    if let Err(
err2
) = api.invalidate_console_window() {
967
2
        warn!("Failed to invalidate console window after recolor: {}", err);
968
6
    }
969
8
}
970
971
/// Empties the console screen output buffer of the current console window using the provided API.
972
///
973
/// # Arguments
974
///
975
/// * `api` - The Windows API implementation to use.
976
///
977
/// # Examples
978
///
979
/// ```no_run
980
/// use csshw_lib::utils::windows::{clear_screen, DefaultWindowsApi};
981
///
982
/// let api = DefaultWindowsApi;
983
/// clear_screen(&api);
984
/// ```
985
7
pub fn clear_screen(api: &dyn WindowsApi) {
986
7
    let buffer_info = api.get_console_screen_buffer_info().unwrap();
987
7
    let scroll_rect = SMALL_RECT {
988
7
        Left: 0,
989
7
        Top: 0,
990
7
        Right: buffer_info.dwSize.X,
991
7
        Bottom: buffer_info.dwSize.Y,
992
7
    };
993
7
    let scroll_target = COORD {
994
7
        X: buffer_info.dwSize.X,
995
7
        Y: 0 - buffer_info.dwSize.Y,
996
7
    };
997
7
    let mut char_info = CHAR_INFO::default();
998
7
    char_info.Char.UnicodeChar = ' ' as u16;
999
7
    char_info.Attributes = buffer_info.wAttributes.0;
1000
1001
7
    api.scroll_console_screen_buffer(scroll_rect, scroll_target, char_info)
1002
7
        .unwrap();
1003
1004
7
    let cursor_position = COORD { X: 0, Y: 0 };
1005
7
    api.set_console_cursor_position(cursor_position).unwrap();
1006
7
}
1007
1008
/// Sets the border color of the current console window using the provided APIs.
1009
///
1010
/// Windows10 does not support this.
1011
///
1012
/// # Arguments
1013
///
1014
/// * `api` - The Windows API implementation;
1015
/// * `color` - RGB [COLORREF][1] to set as border color.
1016
///
1017
/// # Examples
1018
///
1019
/// ```no_run
1020
/// use csshw_lib::utils::windows::{set_console_border_color, DefaultWindowsApi};
1021
/// use windows::Win32::Foundation::COLORREF;
1022
///
1023
/// set_console_border_color(&DefaultWindowsApi, COLORREF(0x001A2B3C));
1024
/// ```
1025
///
1026
/// [1]: https://learn.microsoft.com/en-us/windows/win32/gdi/colorref
1027
3
pub fn set_console_border_color(api: &dyn WindowsApi, color: COLORREF) {
1028
3
    if !is_windows_10(api) {
1029
2
        api.set_console_border_color(&color).unwrap();
1030
2
    
}1
1031
3
}
1032
1033
/// Converts a UTF-16 buffer to a Rust String, filtering out null characters.
1034
///
1035
/// # Arguments
1036
///
1037
/// * `buffer` - The UTF-16 buffer to convert.
1038
///
1039
/// # Returns
1040
///
1041
/// The converted string.
1042
///
1043
/// # Examples
1044
///
1045
/// ```
1046
/// use csshw_lib::utils::windows::utf16_buffer_to_string;
1047
///
1048
/// let utf16_data = vec![72, 101, 108, 108, 111, 0]; // "Hello" + null terminator
1049
/// let result = utf16_buffer_to_string(&utf16_data);
1050
/// assert_eq!(result, "Hello");
1051
/// ```
1052
5
pub fn utf16_buffer_to_string(buffer: &[u16]) -> String {
1053
5
    let vec: Vec<u16> = buffer
1054
5
        .iter()
1055
5
        .copied()
1056
49
        .
filter5
(|val| return *val != 0u16)
1057
5
        .collect();
1058
5
    return String::from_utf16(&vec).unwrap_or_else(|err| 
{0
1059
0
        error!("{}", err);
1060
0
        panic!("Failed to convert UTF-16 buffer to string, invalid utf16")
1061
    });
1062
5
}
1063
1064
/// Returns the title of the current console window using the provided API.
1065
///
1066
/// # Arguments
1067
///
1068
/// * `api` - The Windows API implementation to use.
1069
///
1070
/// # Returns
1071
///
1072
/// The title of the current console window.
1073
///
1074
/// # Examples
1075
///
1076
/// ```no_run
1077
/// use csshw_lib::utils::windows::{get_console_title, DefaultWindowsApi};
1078
///
1079
/// let title = get_console_title(&DefaultWindowsApi);
1080
/// println!("Console title: {}", title);
1081
/// ```
1082
0
pub fn get_console_title(api: &dyn WindowsApi) -> String {
1083
0
    let mut title: [u16; MAX_WINDOW_TITLE_LENGTH] = [0; MAX_WINDOW_TITLE_LENGTH];
1084
0
    api.get_console_title(&mut title);
1085
0
    return utf16_buffer_to_string(&title);
1086
0
}
1087
1088
/// Returns a [HANDLE] to the requested [STD_HANDLE] of the current process.
1089
///
1090
/// # Arguments
1091
///
1092
/// * `nstdhandle` - The standard handle to retrieve.
1093
///                  Either [STD_INPUT_HANDLE] or [STD_OUTPUT_HANDLE].
1094
///
1095
/// # Returns
1096
///
1097
/// The [HANDLE] to the requested [STD_HANDLE].
1098
#[cfg_attr(coverage_nightly, coverage(off))]
1099
fn get_std_handle(nstdhandle: STD_HANDLE) -> HANDLE {
1100
    return unsafe {
1101
        GetStdHandle(nstdhandle)
1102
            .unwrap_or_else(|_| panic!("Failed to retrieve standard handle: {nstdhandle:?}"))
1103
    };
1104
}
1105
1106
/// Returns a [HANDLE] to the [STD_INPUT_HANDLE] of the current process.
1107
///
1108
/// # Returns
1109
///
1110
/// Handle to the standard input of the current process.
1111
///
1112
/// # Examples
1113
///
1114
/// ```no_run
1115
/// use csshw_lib::utils::windows::get_console_input_buffer;
1116
///
1117
/// let input_handle = get_console_input_buffer();
1118
/// ```
1119
#[cfg_attr(coverage_nightly, coverage(off))]
1120
pub fn get_console_input_buffer() -> HANDLE {
1121
    return get_std_handle(STD_INPUT_HANDLE);
1122
}
1123
1124
/// Returns a [HANDLE] to the [STD_OUTPUT_HANDLE] of the current process.
1125
///
1126
/// # Returns
1127
///
1128
/// Handle to the standard output of the current process.
1129
///
1130
/// # Examples
1131
///
1132
/// ```no_run
1133
/// use csshw_lib::utils::windows::get_console_output_buffer;
1134
///
1135
/// let output_handle = get_console_output_buffer();
1136
/// ```
1137
#[cfg_attr(coverage_nightly, coverage(off))]
1138
pub fn get_console_output_buffer() -> HANDLE {
1139
    return get_std_handle(STD_OUTPUT_HANDLE);
1140
}
1141
1142
/// Returns a single [INPUT_RECORD] read from the current process stdinput using the provided API.
1143
///
1144
/// Blocks until 1 record was read.
1145
///
1146
/// # Arguments
1147
///
1148
/// * `api` - The Windows API implementation to use.
1149
///
1150
/// # Returns
1151
///
1152
/// A single INPUT_RECORD that was read.
1153
///
1154
/// # Examples
1155
///
1156
/// ```no_run
1157
/// use csshw_lib::utils::windows::{read_console_input, DefaultWindowsApi};
1158
///
1159
/// let api = DefaultWindowsApi;
1160
/// let input_record = read_console_input(&api);
1161
/// ```
1162
5
pub fn read_console_input(api: &dyn WindowsApi) -> INPUT_RECORD {
1163
    const NB_EVENTS: usize = 1;
1164
5
    let mut input_buffer: [INPUT_RECORD; NB_EVENTS] = [INPUT_RECORD::default(); NB_EVENTS];
1165
    loop {
1166
6
        let number_of_events_read = api
1167
6
            .read_console_input(&mut input_buffer)
1168
6
            .expect("Failed to read console input");
1169
6
        if number_of_events_read == NB_EVENTS as u32 {
1170
5
            break;
1171
1
        }
1172
    }
1173
5
    return input_buffer[0];
1174
5
}
1175
1176
/// Returns a single [INPUT_RECORD_0] where `EventType` == [`KEY_EVENT`] using the provided API.
1177
///
1178
/// Blocks until 1 key event record was read.
1179
///
1180
/// # Arguments
1181
///
1182
/// * `api` - The Windows API implementation to use.
1183
///
1184
/// # Returns
1185
///
1186
/// A single INPUT_RECORD_0 with EventType == KEY_EVENT.
1187
///
1188
/// # Examples
1189
///
1190
/// ```no_run
1191
/// use csshw_lib::utils::windows::{read_keyboard_input, DefaultWindowsApi};
1192
///
1193
/// let api = DefaultWindowsApi;
1194
/// let key_event = read_keyboard_input(&api);
1195
/// ```
1196
1
pub fn read_keyboard_input(api: &dyn WindowsApi) -> INPUT_RECORD_0 {
1197
    loop {
1198
2
        let input_record = read_console_input(api);
1199
2
        match input_record.EventType {
1200
            KEY_EVENT => {
1201
1
                return input_record.Event;
1202
            }
1203
            _ => {
1204
1
                continue;
1205
            }
1206
        }
1207
    }
1208
1
}
1209
1210
/// Changes size and position of the current console window using the provided API.
1211
///
1212
/// # Arguments
1213
///
1214
/// * `api` - The Windows API implementation to use.
1215
/// * `x`       - The x coordinate to move the window to.
1216
///               From the top left corner of the screen.
1217
/// * `y`       - The y coordinate to move the window to.
1218
///               From the top left corner of the screen.
1219
/// * `width`   - The width in pixels to resize the window to.
1220
///               In logical scaling.
1221
/// * `height`  - The height in pixels to resize the window to.
1222
///               In logical scaling.
1223
///
1224
/// # Examples
1225
///
1226
/// ```no_run
1227
/// use csshw_lib::utils::windows::{arrange_console, DefaultWindowsApi};
1228
///
1229
/// let api = DefaultWindowsApi;
1230
/// arrange_console(&api, 100, 100, 800, 600);
1231
/// ```
1232
0
pub fn arrange_console(api: &dyn WindowsApi, x: i32, y: i32, width: i32, height: i32) {
1233
    // FIXME: sometimes a daemon or client console isn't being arrange correctly
1234
    // when this simply retrying doesn't solve the issue. Maybe it has something to do
1235
    // with DPI awareness => https://docs.rs/embed-manifest/latest/embed_manifest/
1236
0
    api.arrange_console(x, y, width, height).unwrap();
1237
0
}
1238
1239
/// Detects if the current windows installation is Windows 10 or not using the provided API.
1240
///
1241
/// Uses the os version, Windows 10 is < `10._.22000`. Windows 11 started with build 22000.
1242
///
1243
/// # Arguments
1244
///
1245
/// * `api` - The Windows API implementation to use.
1246
///
1247
/// # Returns
1248
///
1249
/// Whether the current windows installation is Windows 10 or not.
1250
///
1251
/// # Examples
1252
///
1253
/// ```no_run
1254
/// use csshw_lib::utils::windows::{is_windows_10, DefaultWindowsApi};
1255
///
1256
/// if is_windows_10(&DefaultWindowsApi) {
1257
///     println!("Running on Windows 10");
1258
/// } else {
1259
///     println!("Running on Windows 11 or newer");
1260
/// }
1261
/// ```
1262
10
pub fn is_windows_10(api: &dyn WindowsApi) -> bool {
1263
10
    let version = api.get_os_version();
1264
10
    let mut iter = version.split('.');
1265
10
    let (major, _, build) = (
1266
10
        iter.next().unwrap().parse::<usize>().unwrap(),
1267
10
        iter.next().unwrap().parse::<usize>().unwrap(),
1268
10
        iter.next().unwrap().parse::<usize>().unwrap(),
1269
10
    );
1270
10
    return major < 10 || (
major == 109
&&
build < 220007
);
1271
10
}
1272
1273
#[cfg(test)]
1274
#[path = "../tests/utils/test_windows.rs"]
1275
mod test_mod;